package ace.cmp.spring.graphql.data.jdbc.querydsl.core.controller;

import ace.cmp.data.spring.data.jdbc.querydsl.rsql.core.manager.Manager;
import ace.cmp.data.spring.data.jdbc.querydsl.rsql.core.manager.impl.DefaultManagerConstructArgs;
import ace.cmp.data.spring.data.jdbc.querydsl.rsql.core.manager.spring.ManagerSpringFactory;
import ace.cmp.spring.data.jdbc.querydsl.core.QuerydslRelationHelper;
import ace.cmp.spring.graphql.core.annotation.SchemaExtendMapping;
import ace.cmp.spring.graphql.core.model.GenericPageContent;
import ace.cmp.spring.graphql.data.jdbc.querydsl.core.controller.converter.PageInputToPageableConverter;
import ace.cmp.spring.graphql.data.jdbc.querydsl.core.controller.model.PageInput;
import ace.cmp.spring.graphql.data.jdbc.querydsl.core.dataloader.GraphqlControllerDataLoaderRegister;
import graphql.schema.DataFetchingEnvironment;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.dataloader.DataLoader;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;

/**
 * 1、common graphql : save,add,modify,remove,get,rsql-search.
 *
 * @author caspar
 *
 */
@Slf4j
public abstract class AbstractGenericGraphql<EntityT extends GenericPageContent<IdT>, IdT extends Serializable, PageInputT extends PageInput>
    implements InitializingBean {

  @Autowired
  private ManagerSpringFactory entityManagerSpringFactory;

  @Autowired
  @Getter(AccessLevel.PROTECTED)
  private QuerydslRelationHelper querydslRelationHelper;

  @Getter(value = AccessLevel.PROTECTED)
  @Setter(value = AccessLevel.PROTECTED)
  private Class<IdT> idClass;

  @Getter(value = AccessLevel.PROTECTED)
  @Setter(value = AccessLevel.PROTECTED)
  private Class<EntityT> entityClass;

  @Getter(value = AccessLevel.PROTECTED)
  @Setter(value = AccessLevel.PROTECTED)
  private Manager<EntityT, IdT> manager;

  protected AbstractGenericGraphql() {
    ResolvableType resolvableType = ResolvableType.forType(this.getClass());
    this.entityClass = (Class<EntityT>) resolvableType.getSuperType().getGeneric(0).getRawClass();
    this.idClass = (Class<IdT>) resolvableType.getSuperType().getGeneric(1).getRawClass();
  }

  protected void initManager() {
    DefaultManagerConstructArgs<EntityT, IdT> args = new DefaultManagerConstructArgs<>();
    args.setIdClass(this.getIdClass());
    args.setEntityClass(this.getEntityClass());

    this.manager = entityManagerSpringFactory.getOrCreate(args);
  }

  @Getter(value = AccessLevel.PROTECTED)
  @Setter(value = AccessLevel.PROTECTED)
  @Autowired
  private PageInputToPageableConverter pageInputToPageableConverter;

  @Autowired
  private GraphqlControllerDataLoaderRegister dataLoaderRegister;

  @Override
  public void afterPropertiesSet() throws Exception {
    if (AnnotatedElementUtils.findAllMergedAnnotations(this.getClass(), Set.of(SchemaExtendMapping.class)).size() != 1) {
      throw new IllegalArgumentException(String.format("%s must add SchemaExtendMapping annotation", this.getClass().getName()));
    }
    this.initManager();
    this.registerDataLoader();
  }

  @QueryMapping("Get")
  public CompletableFuture<EntityT> getById(
      @Valid @NotNull @Argument("id") IdT id, DataFetchingEnvironment dataFetchingEnvironment) {
    DataLoader<IdT, EntityT> dataLoader = this.checkAndGetDataLoader(dataFetchingEnvironment);
    return dataLoader.load(id);
  }

  @QueryMapping("GetList")
  public CompletableFuture<List<EntityT>> getListByIdIn(
      @Valid @NotNull @NotEmpty @Argument("ids") List<IdT> ids,
      DataFetchingEnvironment dataFetchingEnvironment) {
    DataLoader<IdT, EntityT> dataLoader = this.checkAndGetDataLoader(dataFetchingEnvironment);
    return dataLoader.loadMany(ids);
  }

  /**
   * 搜索
   *
   * @param query rsql语法
   * @param page
   * @return
   */
  @QueryMapping("Search")
  public Page<EntityT> search(
      @Argument(value = "query") String query, @Valid @NotNull @Argument("page") PageInputT page) {
    Pageable pageable = this.pageInputToPageableConverter.convert(page);
    Page<EntityT> entityPage = this.getManager().search(query, pageable);
    return entityPage;
  }

  /**
   * 新增或修改
   *
   * @param entity
   * @return
   */
  @MutationMapping("Save")
  public IdT save(
      @Valid @NotNull @Argument("input") EntityT entity,
      @Valid @NotNull @Argument("saveNullFieldFlag") Boolean saveNullFieldFlag
  ) {
    return (IdT) this.getManager().saveIgnore(entity, saveNullFieldFlag);
  }

  /**
   * 新增
   *
   * @param entity
   * @return
   */
  @MutationMapping("Add")
  public IdT add(
      @Valid @NotNull @Argument("input") EntityT entity,
      @Valid @NotNull @Argument("saveNullFieldFlag") Boolean saveNullFieldFlag) {
    return (IdT) this.getManager().addIgnore(entity, saveNullFieldFlag);
  }

  /**
   * 修改
   *
   * @return
   */
  @MutationMapping("Modify")
  public IdT modify(
      @Valid @NotNull @Argument("input") EntityT entity,
      @Valid @NotNull @Argument("saveNullFieldFlag") Boolean saveNullFieldFlag) {
    return (IdT) this.getManager().modify(entity, saveNullFieldFlag);
  }

  /**
   * 实际删除
   *
   * @param ids
   * @return
   */
  @MutationMapping("Remove")
  public Boolean remove(@Valid @NotEmpty @Argument("ids") List<IdT> ids) {
    return this.getManager().remove(ids);
  }

  /**
   * 逻辑删除
   *
   * @param ids
   * @return
   */
  @MutationMapping("RemoveLogic")
  public Boolean removeLogic(@Valid @NotEmpty @Argument("ids") List<IdT> ids) {
    return this.getManager().removeLogic(ids);
  }

  protected DataLoader<IdT, EntityT> checkAndGetDataLoader(
      DataFetchingEnvironment dataFetchingEnvironment) {
    DataLoader<IdT, EntityT> dataLoader =
        dataFetchingEnvironment.getDataLoader(this.getEntityClass().getName());
    if (dataLoader == null) {
      throw new RuntimeException("dataLoader is not define.");
    }
    return dataLoader;
  }

  private void registerDataLoader() {
    dataLoaderRegister.register((Class<IdT>) this.getIdClass(), this.getEntityClass(), this::getMapByIdIn);
  }

  protected Map<IdT, EntityT> getMapByIdIn(List<IdT> ids) {
    return (Map<IdT, EntityT>) this.getManager().getMapByIdIn(ids);
  }
}
